export const enum Kinds {
  Null = 'Null',
  Undefined = 'Undefined',
  Boolean = 'Boolean',
  Number = 'Number',
  String = 'String',
  Date = 'Date',
  RegExp = 'RegExp',
  Function = 'Function',
  Arguments = 'Arguments',
  Symbol = 'Symbol',
  Object = 'Object',
  Array = 'Array',
  Set = 'Set',
  Map = 'Map',
}

/**
 * Recursively clone native types.
 * Object.prototype.toString.call(1) // '[object Number]'
 * Object.prototype.toString.call('a') // '[object String]'
 * Object.prototype.toString.call(true) // '[object Boolean]'
 * Object.prototype.toString.call(new Set()) // '[object Set]'
 * Object.prototype.toString.call(new Map()) // '[object Map]'
 * Object.prototype.toString.call(new Date()) // '[object Date]'
 */
export function kindOf(val: unknown): string {
  if (val === null) {
    return Kinds.Null
  } else if (val === undefined) {
    return Kinds.Undefined
  } else {
    const arr = /^\[object (.*)]$/.exec(Object.prototype.toString.call(val))
    return arr == null ? Kinds.Undefined : arr[1]
  }
}

/**
 * Check if value is from a specific "kind".
 */
function _isKind(val: unknown, type: string): boolean {
  return kindOf(val) === type
}

export function isNull(val: unknown): boolean {
  return val === null
}

export function isUndefined(val: unknown): boolean {
  return val === undefined
}

/**
 * isNil(null) => true
 * isNil(void 0) => true
 * isNil(NaN) => false
 */
export function isNil(val: unknown): boolean {
  return val == null
}

export function isArguments(val: unknown) {
  return kindOf(val) === Kinds.Arguments
}

// export const isArray = Array.isArray

export function isArray(val: unknown) {
  return Array.isArray || kindOf(val) === Kinds.Array // 兼容 ES5 和 ES6 的判断是否是数组的方法
}

export function isBoolean(val: any): boolean {
  return kindOf(val) === Kinds.Boolean
}

export function isDate(val: any): boolean {
  return kindOf(val) === Kinds.Date
}

export function isEmpty(val: any): boolean {
  // typeof null == 'object' so we check it first
  if (val == null) {
    return false
  } else if (typeof val === 'string' || isArray(val)) {
    return !val.length
  } else if (typeof val === 'object' || typeof val === 'function') {
    for (const key in val) {
      if (val.hasOwnProperty(key)) {
        return false
      }
    }
    return true
  } else {
    return false
  }
}

export function isFunction(val: unknown): val is Function {
  return typeof val === 'function'
  // return kindOf(val) === Kinds.Function
}

export function isString(val: unknown): boolean {
  return typeof val === 'string'
  // return kindOf(val) === Kinds.String
}

export function isSymbol(val: unknown): boolean {
  return typeof val === 'symbol'
  // return kindOf(val) === Kinds.Symbol
}

export function isNumber(val: unknown): boolean {
  return _isKind(val, Kinds.Number)
}

export function isInteger(val: unknown): boolean {
  return isNumber(val) && ((val as number) % 1 === 0)
}

export function isObject(val: unknown): val is Record<any, any> {
  return val !== null && typeof val === 'object'
  // return kindOf(val) === Kinds.Object
}

export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
  return (
    (isObject(val) || isFunction(val)) &&
    isFunction((val as any).then) &&
    isFunction((val as any).catch)
  )
}

/**
 * Checks if the value is created by the `Object` constructor.
 */
export function isPlainObject(val: unknown): boolean {
  return !!val && typeof val === 'object' && val.constructor === Object
}

export function isRegExp(val: unknown): boolean {
  return _isKind(val, Kinds.RegExp)
}

export function isNaN(val: unknown): boolean {
  // based on the fact that NaN !== NaN
  // need to check if it's a number to avoid conflicts with host objects
  // also need to coerce ToNumber to avoid edge case `new Number(NaN)`
  /* eslint eqeqeq: off */
  return !isNumber(val) || Number.isNaN(val)
}

/**
 * Check if value is finite
 */
export function isFinite(val: unknown): boolean {
  let is = false
  if (typeof val === 'string' && val !== '') {
    is = Number.isFinite(parseFloat(val))
  } else if (isNumber(val)) {
    // need to use isNumber because of Number constructor
    is = Number.isFinite(val)
  }
  return is
}

export function isStringNumber(val: unknown): boolean {
  if (!isString(val)) {
    return false
  }
  return !Number.isNaN(Number(val))
}

export const isElement = (e: unknown): e is Element => {
  if (typeof Element === 'undefined') return false
  return e instanceof Element
}
